/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2017-2025 Ta4j Organization & respective
 * authors (see AUTHORS)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package org.ta4j.core.serialization;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import org.ta4j.core.BarSeries;
import org.ta4j.core.BaseStrategy;
import org.ta4j.core.Rule;
import org.ta4j.core.Strategy;
import org.ta4j.core.strategy.named.NamedStrategy;

/**
 * Serializes and deserializes {@link Strategy} instances into structured
 * {@link ComponentDescriptor} payloads.
 *
 * @since 0.19
 */
public final class StrategySerialization {

    private static final String ENTRY_LABEL = "entry";
    private static final String EXIT_LABEL = "exit";
    private static final String STRATEGY_PACKAGE = "org.ta4j.core";
    private static final String UNSTABLE_BARS_KEY = "unstableBars";
    private static final String ARGS_KEY = "__args";

    private StrategySerialization() {
    }

    /**
     * Serializes a {@link Strategy} to a JSON payload.
     *
     * @param strategy strategy instance
     * @return JSON representation
     */
    public static String toJson(Strategy strategy) {
        return ComponentSerialization.toJson(describe(strategy));
    }

    /**
     * Converts a {@link Strategy} into a {@link ComponentDescriptor} hierarchy.
     *
     * @param strategy strategy instance
     * @return descriptor representing the strategy
     */
    public static ComponentDescriptor describe(Strategy strategy) {
        Objects.requireNonNull(strategy, "strategy");

        if (strategy instanceof NamedStrategy) {
            return strategy.toDescriptor();
        }

        ComponentDescriptor entryDescriptor = RuleSerialization.describe(strategy.getEntryRule());
        ComponentDescriptor exitDescriptor = RuleSerialization.describe(strategy.getExitRule());

        Class<?> strategyClass = strategy.getClass();
        String typeName = strategyClass.getPackageName().equals(STRATEGY_PACKAGE) ? strategyClass.getSimpleName()
                : strategyClass.getName();
        ComponentDescriptor.Builder builder = ComponentDescriptor.builder().withType(typeName);

        String name = strategy.getName();
        if (name != null && !name.isBlank()) {
            builder.withLabel(name);
        }

        Map<String, Object> parameters = new HashMap<>();
        parameters.put(UNSTABLE_BARS_KEY, strategy.getUnstableBars());
        builder.withParameters(parameters);

        if (entryDescriptor != null) {
            builder.addComponent(applyLabel(entryDescriptor, ENTRY_LABEL));
        }
        if (exitDescriptor != null) {
            builder.addComponent(applyLabel(exitDescriptor, EXIT_LABEL));
        }

        return builder.build();
    }

    /**
     * Rebuilds a strategy from a JSON payload.
     *
     * @param series bar series to attach to the strategy
     * @param json   JSON representation generated by {@link #toJson(Strategy)}
     * @return reconstructed strategy
     */
    public static Strategy fromJson(BarSeries series, String json) {
        ComponentDescriptor descriptor = ComponentSerialization.parse(json);
        return fromDescriptor(series, descriptor);
    }

    /**
     * Rebuilds a strategy from a descriptor tree.
     * <p>
     * If the specified strategy type cannot be instantiated (e.g., no matching
     * constructor is found), this method will silently fall back to creating a
     * {@link BaseStrategy} instance with the same entry/exit rules and parameters.
     * This fallback behavior may mask configuration issues where a specific
     * strategy type was expected but could not be constructed. Callers should
     * verify the returned strategy type matches expectations if strict type
     * checking is required.
     *
     * @param series     bar series to attach to the strategy
     * @param descriptor descriptor describing the strategy
     * @return reconstructed strategy (may be a {@link BaseStrategy} fallback if the
     *         specified type could not be instantiated)
     */
    public static Strategy fromDescriptor(BarSeries series, ComponentDescriptor descriptor) {
        Objects.requireNonNull(series, "series");
        Objects.requireNonNull(descriptor, "descriptor");

        String descriptorType = descriptor.getType();
        if (NamedStrategy.SERIALIZED_TYPE.equals(descriptorType)
                || NamedStrategy.class.getName().equals(descriptorType)) {
            return instantiateNamedStrategy(series, descriptor, null);
        }

        Class<? extends Strategy> strategyType = resolveStrategyClass(descriptorType);
        if (NamedStrategy.class.isAssignableFrom(strategyType)) {
            return instantiateNamedStrategy(series, descriptor, strategyType);
        }

        // Create a Strategy-level context that contains all Strategy components
        // This allows rule deserialization to resolve Strategy-level indicators and
        // rules
        // For now, pass null as parent context - rule components contain all their
        // dependencies
        // and don't need Strategy-level resolution during constructor matching
        Rule entryRule = instantiateRule(series, extractChild(descriptor, ENTRY_LABEL), null);
        Rule exitRule = instantiateRule(series, extractChild(descriptor, EXIT_LABEL), null);

        String name = descriptor.getLabel();
        int unstableBars = extractUnstableBars(descriptor.getParameters().get(UNSTABLE_BARS_KEY));

        Strategy strategy = instantiateStrategy(strategyType, name, entryRule, exitRule, unstableBars);
        strategy.setUnstableBars(unstableBars);
        return strategy;
    }

    private static ComponentDescriptor applyLabel(ComponentDescriptor descriptor, String label) {
        if (descriptor == null) {
            return null;
        }
        ComponentDescriptor.Builder builder = ComponentDescriptor.builder()
                .withType(descriptor.getType())
                .withLabel(label);
        if (!descriptor.getParameters().isEmpty()) {
            builder.withParameters(descriptor.getParameters());
        }
        for (ComponentDescriptor component : descriptor.getComponents()) {
            builder.addComponent(component);
        }
        return builder.build();
    }

    private static ComponentDescriptor extractChild(ComponentDescriptor descriptor, String label) {
        for (ComponentDescriptor component : descriptor.getComponents()) {
            if (label.equals(component.getLabel())) {
                return cloneWithoutLabel(component);
            }
        }
        throw new IllegalArgumentException("Missing strategy " + label + " rule descriptor");
    }

    private static ComponentDescriptor cloneWithoutLabel(ComponentDescriptor descriptor) {
        if (descriptor == null) {
            return null;
        }
        ComponentDescriptor.Builder builder = ComponentDescriptor.builder().withType(descriptor.getType());
        if (!descriptor.getParameters().isEmpty()) {
            builder.withParameters(descriptor.getParameters());
        }
        for (ComponentDescriptor component : descriptor.getComponents()) {
            builder.addComponent(component);
        }
        return builder.build();
    }

    private static int extractUnstableBars(Object value) {
        if (value == null) {
            return 0;
        }
        if (value instanceof Number) {
            return ((Number) value).intValue();
        }
        try {
            return Integer.parseInt(String.valueOf(value));
        } catch (NumberFormatException e) {
            return 0;
        }
    }

    private static Rule instantiateRule(BarSeries series, ComponentDescriptor descriptor) {
        return instantiateRule(series, descriptor, null);
    }

    private static Rule instantiateRule(BarSeries series, ComponentDescriptor descriptor,
            RuleSerialization.ReconstructionContext parentContext) {
        if (descriptor == null) {
            throw new IllegalArgumentException("Rule descriptor cannot be null");
        }

        // Check if this is a rule by type name (contains "Rule")
        String type = descriptor.getType();
        if (type != null && type.contains("Rule")) {
            return RuleSerialization.fromDescriptor(series, descriptor, parentContext);
        }

        // Legacy check for __args (for backwards compatibility)
        if (descriptor.getParameters().containsKey(ARGS_KEY)) {
            return RuleSerialization.fromDescriptor(series, descriptor, parentContext);
        }

        if (type == null || type.isBlank()) {
            throw new IllegalArgumentException("Rule descriptor missing type: " + descriptor);
        }
        Class<?> clazz = resolveRuleClass(type);
        if (!Rule.class.isAssignableFrom(clazz)) {
            throw new IllegalArgumentException("Descriptor type does not implement Rule: " + type);
        }
        @SuppressWarnings("unchecked")
        Class<? extends Rule> ruleType = (Class<? extends Rule>) clazz;

        Optional<Rule> instance = invokeFactory(ruleType, series, descriptor);
        if (instance.isPresent()) {
            return instance.get();
        }

        if (!descriptor.getComponents().isEmpty()) {
            List<Rule> components = new ArrayList<>(descriptor.getComponents().size());
            for (ComponentDescriptor component : descriptor.getComponents()) {
                components.add(instantiateRule(series, component, parentContext));
            }
            Optional<Rule> composite = tryCompositeConstructor(ruleType, components);
            if (composite.isPresent()) {
                return composite.get();
            }
        }

        Optional<Rule> constructed = tryDescriptorConstructor(ruleType, series, descriptor);
        if (constructed.isPresent()) {
            return constructed.get();
        }

        throw new IllegalArgumentException("Unable to instantiate rule type: " + type);
    }

    private static Optional<Rule> invokeFactory(Class<? extends Rule> ruleType, BarSeries series,
            ComponentDescriptor descriptor) {
        try {
            Method factory = ruleType.getDeclaredMethod("fromDescriptor", BarSeries.class, ComponentDescriptor.class);
            factory.setAccessible(true);
            return Optional.of((Rule) factory.invoke(null, series, descriptor));
        } catch (NoSuchMethodException ex) {
            // ignore and try the next option
        } catch (IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to invoke factory on rule type: " + ruleType.getName(), ex);
        }

        try {
            Method factory = ruleType.getDeclaredMethod("fromDescriptor", ComponentDescriptor.class);
            factory.setAccessible(true);
            return Optional.of((Rule) factory.invoke(null, descriptor));
        } catch (NoSuchMethodException ex) {
            // ignore and try next
        } catch (IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to invoke factory on rule type: " + ruleType.getName(), ex);
        }

        return Optional.empty();
    }

    private static Optional<Rule> tryCompositeConstructor(Class<? extends Rule> ruleType, List<Rule> components) {
        Constructor<?>[] constructors = ruleType.getDeclaredConstructors();
        for (Constructor<?> constructor : constructors) {
            Class<?>[] parameterTypes = constructor.getParameterTypes();
            if (parameterTypes.length != components.size()) {
                continue;
            }
            boolean matches = true;
            for (Class<?> parameterType : parameterTypes) {
                if (!Rule.class.isAssignableFrom(parameterType)) {
                    matches = false;
                    break;
                }
            }
            if (!matches) {
                continue;
            }
            try {
                constructor.setAccessible(true);
                return Optional.of((Rule) constructor.newInstance(components.toArray()));
            } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
                throw new IllegalStateException("Failed to construct composite rule: " + ruleType.getName(), ex);
            }
        }
        return Optional.empty();
    }

    private static Optional<Rule> tryDescriptorConstructor(Class<? extends Rule> ruleType, BarSeries series,
            ComponentDescriptor descriptor) {
        try {
            Constructor<? extends Rule> constructor = ruleType.getDeclaredConstructor(BarSeries.class,
                    ComponentDescriptor.class);
            constructor.setAccessible(true);
            return Optional.of(constructor.newInstance(series, descriptor));
        } catch (NoSuchMethodException ex) {
            // ignore
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to construct rule: " + ruleType.getName(), ex);
        }

        try {
            Constructor<? extends Rule> constructor = ruleType.getDeclaredConstructor(ComponentDescriptor.class);
            constructor.setAccessible(true);
            return Optional.of(constructor.newInstance(descriptor));
        } catch (NoSuchMethodException ex) {
            // ignore
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to construct rule: " + ruleType.getName(), ex);
        }

        try {
            Constructor<? extends Rule> constructor = ruleType.getDeclaredConstructor();
            constructor.setAccessible(true);
            return Optional.of(constructor.newInstance());
        } catch (NoSuchMethodException ex) {
            // ignore
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to construct rule: " + ruleType.getName(), ex);
        }

        return Optional.empty();
    }

    @SuppressWarnings("unchecked")
    private static Class<? extends Rule> resolveRuleClass(String type) {
        try {
            return (Class<? extends Rule>) Class.forName(type);
        } catch (ClassNotFoundException ex) {
            // try ta4j core rules package
        }
        try {
            return (Class<? extends Rule>) Class.forName("org.ta4j.core.rules." + type);
        } catch (ClassNotFoundException ex) {
            throw new IllegalArgumentException("Unknown rule type: " + type, ex);
        }
    }

    @SuppressWarnings("unchecked")
    private static Class<? extends Strategy> resolveStrategyClass(String type) {
        if (type == null || type.isBlank()) {
            return BaseStrategy.class;
        }
        try {
            Class<?> clazz = Class.forName(type);
            if (Strategy.class.isAssignableFrom(clazz)) {
                return (Class<? extends Strategy>) clazz;
            }
        } catch (ClassNotFoundException ex) {
            // ignore and try package-local lookup
        }
        try {
            Class<?> clazz = Class.forName(STRATEGY_PACKAGE + '.' + type);
            if (Strategy.class.isAssignableFrom(clazz)) {
                return (Class<? extends Strategy>) clazz;
            }
        } catch (ClassNotFoundException ex) {
            // ignore and fall back to BaseStrategy
        }
        return BaseStrategy.class;
    }

    /**
     * Attempts to instantiate a strategy of the specified type using various
     * constructor patterns.
     * <p>
     * This method tries multiple constructor signatures in order:
     * <ol>
     * <li>{@code (String, Rule, Rule, int)} - name, entry, exit, unstableBars</li>
     * <li>{@code (String, Rule, Rule)} - name, entry, exit (unstableBars set via
     * setter)</li>
     * <li>{@code (Rule, Rule, int)} - entry, exit, unstableBars</li>
     * <li>{@code (Rule, Rule)} - entry, exit (unstableBars set via setter)</li>
     * </ol>
     * <p>
     * <strong>Fallback Behavior:</strong> If none of the above constructors are
     * found and the requested type is not {@link BaseStrategy}, this method will
     * silently create a {@link BaseStrategy} instance instead. This fallback
     * provides resilience but may mask configuration issues where a specific
     * strategy type was expected. The returned instance will have the same entry
     * rule, exit rule, name, and unstableBars value, but will be of type
     * {@code BaseStrategy} rather than the requested type.
     * <p>
     * If the requested type is already {@code BaseStrategy} and no suitable
     * constructor is found, an {@link IllegalStateException} is thrown.
     *
     * @param strategyType the class of strategy to instantiate
     * @param name         strategy name (may be null)
     * @param entryRule    entry rule (required)
     * @param exitRule     exit rule (required)
     * @param unstableBars number of unstable bars
     * @return instantiated strategy (may be a {@link BaseStrategy} fallback if the
     *         requested type could not be instantiated)
     * @throws IllegalStateException if the requested type is {@code BaseStrategy}
     *                               and no suitable constructor is found
     */
    private static Strategy instantiateStrategy(Class<? extends Strategy> strategyType, String name, Rule entryRule,
            Rule exitRule, int unstableBars) {
        try {
            Constructor<? extends Strategy> constructor = strategyType.getDeclaredConstructor(String.class, Rule.class,
                    Rule.class, int.class);
            constructor.setAccessible(true);
            return constructor.newInstance(name, entryRule, exitRule, unstableBars);
        } catch (NoSuchMethodException ex) {
            // ignore and try the next options
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to construct strategy: " + strategyType.getName(), ex);
        }

        try {
            Constructor<? extends Strategy> constructor = strategyType.getDeclaredConstructor(String.class, Rule.class,
                    Rule.class);
            constructor.setAccessible(true);
            Strategy strategy = constructor.newInstance(name, entryRule, exitRule);
            strategy.setUnstableBars(unstableBars);
            return strategy;
        } catch (NoSuchMethodException ex) {
            // ignore
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to construct strategy: " + strategyType.getName(), ex);
        }

        try {
            Constructor<? extends Strategy> constructor = strategyType.getDeclaredConstructor(Rule.class, Rule.class,
                    int.class);
            constructor.setAccessible(true);
            Strategy strategy = constructor.newInstance(entryRule, exitRule, unstableBars);
            return strategy;
        } catch (NoSuchMethodException ex) {
            // ignore
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to construct strategy: " + strategyType.getName(), ex);
        }

        try {
            Constructor<? extends Strategy> constructor = strategyType.getDeclaredConstructor(Rule.class, Rule.class);
            constructor.setAccessible(true);
            Strategy strategy = constructor.newInstance(entryRule, exitRule);
            strategy.setUnstableBars(unstableBars);
            return strategy;
        } catch (NoSuchMethodException ex) {
            // ignore
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to construct strategy: " + strategyType.getName(), ex);
        }

        // Fallback: If the requested strategy type cannot be instantiated, create a
        // BaseStrategy instead. This provides resilience but may mask configuration
        // issues where a specific strategy type was expected. The fallback preserves
        // the entry/exit rules and parameters but changes the strategy type.
        if (!strategyType.equals(BaseStrategy.class)) {
            return new BaseStrategy(name, entryRule, exitRule, unstableBars);
        }
        throw new IllegalStateException("No suitable constructor found for strategy type: " + strategyType.getName());
    }

    private static Strategy instantiateNamedStrategy(BarSeries series, ComponentDescriptor descriptor,
            Class<? extends Strategy> resolvedType) {
        String label = descriptor.getLabel();
        if (label == null || label.isBlank()) {
            throw new IllegalArgumentException("Named strategy descriptor missing label");
        }

        List<String> labelTokens = NamedStrategy.splitLabel(label);
        if (labelTokens.isEmpty()) {
            throw new IllegalArgumentException("Named strategy label missing strategy identifier");
        }

        String simpleName = labelTokens.get(0);
        String[] parameters = labelTokens.size() == 1 ? new String[0]
                : labelTokens.subList(1, labelTokens.size()).toArray(new String[0]);

        Class<? extends NamedStrategy> strategyType = resolveNamedStrategyType(simpleName, resolvedType);
        Constructor<? extends Strategy> constructor = findNamedStrategyConstructor(strategyType);
        try {
            return constructor.newInstance(new Object[] { series, parameters });
        } catch (InstantiationException | IllegalAccessException ex) {
            throw new IllegalStateException("Failed to construct named strategy: " + strategyType.getName(), ex);
        } catch (InvocationTargetException ex) {
            Throwable cause = ex.getCause();
            if (cause instanceof IllegalArgumentException) {
                throw (IllegalArgumentException) cause;
            }
            throw new IllegalStateException("Failed to construct named strategy: " + strategyType.getName(), cause);
        }
    }

    @SuppressWarnings("unchecked")
    private static Class<? extends NamedStrategy> resolveNamedStrategyType(String simpleName,
            Class<? extends Strategy> resolvedType) {
        if (resolvedType != null && resolvedType != NamedStrategy.class
                && NamedStrategy.class.isAssignableFrom(resolvedType)) {
            return (Class<? extends NamedStrategy>) resolvedType;
        }
        return NamedStrategy.requireRegistered(simpleName);
    }

    private static Constructor<? extends Strategy> findNamedStrategyConstructor(
            Class<? extends NamedStrategy> strategyType) {
        try {
            Constructor<? extends Strategy> constructor = strategyType.getDeclaredConstructor(BarSeries.class,
                    String[].class);
            constructor.setAccessible(true);
            return constructor;
        } catch (NoSuchMethodException ex) {
            throw new IllegalStateException(
                    "Named strategy missing (BarSeries, String...) constructor: " + strategyType.getName(), ex);
        }
    }

}
